package com.th.coupon.service.impl;

import com.alibaba.fastjson.JSON;
import com.th.coupon.common.constant.Constant;
import com.th.coupon.common.exception.BaseCloudException;
import com.th.coupon.common.vo.CommonResponse;
import com.th.coupon.common.vo.CouponTemplateSdk;
import com.th.coupon.common.vo.GoodsInfo;
import com.th.coupon.common.vo.SettlementInfo;
import com.th.coupon.common.vo.response.ExceptionResultData;
import com.th.coupon.constant.CouponStatus;
import com.th.coupon.dao.CouponDao;
import com.th.coupon.entity.Coupon;
import com.th.coupon.feign.SettlementClient;
import com.th.coupon.feign.TemplateClient;
import com.th.coupon.service.IRedisService;
import com.th.coupon.service.IUserService;
import com.th.coupon.vo.AcquireTemplateRequest;
import com.th.coupon.vo.CouponClassify;
import com.th.coupon.vo.CouponRabbitMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 用户服务相关实现
 * 所有的操作过程以及，状态都保存在redis中，并通过rabbitMq把消息传递到Mysql中
 * 为什么使用消息对列而不是异步服务，安全问题，异步任务是会造成失败的，传递到消息队列中的消息失败发送，也可以重新从消息队列中获取消息记录，保证redis中的一致性
 * @author 涂航
 */
@Service
@Slf4j
public class UserServiceImpl implements IUserService {

    private final IRedisService redisService;

    private final AmqpTemplate amqpTemplate;

    private final SettlementClient settlementClient;

    private final TemplateClient templateClient;

    private final CouponDao couponDao;

    @Autowired
    public UserServiceImpl(IRedisService redisService, AmqpTemplate amqpTemplate, SettlementClient settlementClient, TemplateClient templateClient, CouponDao couponDao) {
        this.redisService = redisService;
        this.amqpTemplate = amqpTemplate;
        this.settlementClient = settlementClient;
        this.templateClient = templateClient;
        this.couponDao = couponDao;
    }

    /**
     * 根据用户 id以及状态查询优惠卷
     * @param userId 用户id
     * @param status 用户状态
     * @return
     * @throws BaseCloudException
     */
    @Override
    public List<Coupon> findCouponsByStatus(Long userId, Integer status) throws BaseCloudException {
        //查询缓存中的优惠卷
        List<Coupon> cachedCoupons = redisService.getCachedCoupons(userId, status);
        List<Coupon> preTarget;
        if(CollectionUtils.isNotEmpty(cachedCoupons)){
            //缓存存在
            log.debug("coupon cache is not empty : {} ,{}",userId,status);
            preTarget = cachedCoupons;
        }else {
            //缓存不存在
            log.debug("coupon cache is empty ,get coupon from db: {} ,{}",userId,status);
            List<Coupon> dbCoupons = couponDao.findAllByUserIdAndStatus(userId, CouponStatus.of(status));
            //如果数据库中没有记录，直接返回即可，Cache 中已经加入一张无效的优惠卷
            if(CollectionUtils.isEmpty(dbCoupons)){
                log.debug("current user do not have coupon: {} ,{}",userId,status);
                return  dbCoupons;
            }
            //填充dbCoupons 的 CouponTemplateSdk 字段
            Map<Integer, CouponTemplateSdk> ids2TemplateSdk = templateClient
                    .findIds2TemplateSdk(
                            dbCoupons.stream().map(Coupon::getId).collect(Collectors.toList())).getData();
            dbCoupons.forEach(dbCoupon->{
                dbCoupon.setTemplateSDK(ids2TemplateSdk.get(dbCoupon.getId()));
            });
            preTarget = dbCoupons;
            //将记录写入缓存 redis
            redisService.addCouponToCache(userId,preTarget,status);
        }
        //将无效优惠剔除
        preTarget = preTarget.stream().filter(c->  c.getId() != -1 ).collect(Collectors.toList());
        //如果当前获取的是可用优惠卷 ，还需要对已过期优惠卷做延迟处理
        if(CouponStatus.of(status) == CouponStatus.USABLE){
            CouponClassify classify = CouponClassify.classify(preTarget);
            //如果已过期状态不为空 ，需要做延迟处理
            if(CollectionUtils.isNotEmpty(classify.getExpired())){
                log.info("Add Expired Coupons To Cache From findCouponsByStatus :{} ,{}",userId,status);
                redisService.addCouponToCache(
                        userId,classify.getExpired(),
                        CouponStatus.EXPIRED.getCode());
                //发送到rabbitMq中做异步处理
                amqpTemplate.convertAndSend(Constant.RabbitMq.DISCOUNT_USER_COUPON_EXCHANGE
                ,Constant.RabbitMq.DISCOUNT_USER_COUPON_KEY
                ,new CouponRabbitMessage(
                        CouponStatus.EXPIRED.getCode(),
                                classify.getExpired().stream().map(Coupon::getId).collect(Collectors.toList())));
            }
            //返回可用优惠卷
            return classify.getUsable();
        }
        return preTarget;
    }

    /**
     * 查询可用（可领取）的优惠卷模板
     * @param userId  用户id
     * @return
     * @throws BaseCloudException
     */
    @Override
    public List<CouponTemplateSdk> findAvailableTemplate(Long userId) throws BaseCloudException {
        Long curTime = System.currentTimeMillis();
        //获取可用的优惠卷模板
        List<CouponTemplateSdk> templateSdks = templateClient.findAllUsableTemplate().getData();

        log.debug("Find All Template(From TemplateClient) Count: {}",templateSdks.size());

        //过滤过期的优惠卷模板
        templateSdks = templateSdks.stream().filter(t -> t.getRule().getExpiration().getDeadline() > curTime)
                .collect(Collectors.toList());
        log.info("Find Usable Template Count : {}",templateSdks.size());
        //key 是 TemplateId
        //value 中的 left 是 Template limitation ,right 是优惠卷模板
        Map<Integer, Pair<Integer,CouponTemplateSdk>> limit2Template =
                new HashMap<>(templateSdks.size());
        //该目的是为了判断用户领取的优惠卷数量是否已经达到可领取的上线
        templateSdks.forEach(t -> limit2Template.put(
                t.getId(),
                Pair.of(t.getRule().getLimitation(),t)
        ));
        //待返回的结果
        List<CouponTemplateSdk> result = new ArrayList<>(limit2Template.size());
        //获取用户可用的优惠卷
        List<Coupon> userUsableCoupons = findCouponsByStatus(userId,CouponStatus.USABLE.getCode());
        log.debug("Current User Has Usable Coupons: {} , {}",userId,userUsableCoupons.size());

        //根据模板id将优惠卷分组
        Map<Integer,List<Coupon>> templateId2Coupons = userUsableCoupons
                .stream()
                .collect(Collectors.groupingBy(Coupon::getTemplateId));
        //根据 template Rule 判断是否可以领取优惠卷模板
        limit2Template.forEach((k,v) ->{
            int limitation = v.getLeft();
            CouponTemplateSdk templateSdk = v.getRight();
            //如果用户已经有该优惠卷 则该优惠卷模板不可领取使用
            if(templateId2Coupons.containsKey(k)
                    && templateId2Coupons.get(k).size() >= limitation){
                return;
            }
            result.add(templateSdk);
        });

        return result;
    }

    /**
     * 用户领取优惠卷
     * 1.从TemplateClient 拿到对应的优惠卷，并检查是否过期
     * 2.根据limitation 判断用户是否可以领取
     * 3.save to db
     * 4.填充CouponTemplateSdk
     * 5.加入到redis
     * @param acquireTemplateRequest
     * @return
     * @throws BaseCloudException
     */
    @Override
    public Coupon acquireTemplate(AcquireTemplateRequest acquireTemplateRequest) throws BaseCloudException {
        Map<Integer,CouponTemplateSdk> id2Template =
                templateClient.findIds2TemplateSdk(
                        Collections.singletonList(acquireTemplateRequest.getCouponTemplateSdk().getId())
                ).getData();

        //优惠卷模板是需要存在的
        if(id2Template.size() <= 0){
            log.error("Can Not Acquire Template From TemplateClient: {}"
                    ,acquireTemplateRequest.getCouponTemplateSdk().getId());
            throw new BaseCloudException(ExceptionResultData.NO_ACQUIRE_COUPON);
        }
        //用户是否可以领取这张优惠卷
        List<Coupon> couponsByStatus =
                this.findCouponsByStatus(acquireTemplateRequest.getUserId(), CouponStatus.USABLE.getCode());
        //根据模板id将优惠卷分组
        Map<Integer,List<Coupon>> templateId2Coupons = couponsByStatus
                .stream()
                .collect(Collectors.groupingBy(Coupon::getTemplateId));
        //判断当前领取的优惠卷是否已领取 并且是否超过最大领取限度
        if(templateId2Coupons.containsKey(acquireTemplateRequest.getCouponTemplateSdk().getId())
        && templateId2Coupons.get(acquireTemplateRequest.getCouponTemplateSdk().getId()).size() >=
        acquireTemplateRequest.getCouponTemplateSdk().getRule().getLimitation()){
            log.error("Exceed Template Assign Limitation: {} ",acquireTemplateRequest.getCouponTemplateSdk().getId());
            throw new BaseCloudException(ExceptionResultData.COUPON_EXCEED_ASSIGN_LIMITATION);
        }
        //尝试获取优惠卷码
        String couponCode =
                redisService.tryToAcquireCouponCodeFromCache(acquireTemplateRequest.getCouponTemplateSdk().getId());
        if(StringUtils.isEmpty(couponCode)){
            log.error("Can Not Acquire Coupon Code: {}"
                    ,acquireTemplateRequest.getCouponTemplateSdk().getId());
            throw new BaseCloudException(ExceptionResultData.NO_ACQUIRE_COUPON);
        }
        Coupon newCoupon = new Coupon(acquireTemplateRequest.getCouponTemplateSdk().getId(),
                acquireTemplateRequest.getUserId(),
                couponCode, CouponStatus.USABLE);
        newCoupon = couponDao.save(newCoupon);

        //填充Coupon对象的CouponTemplateSdk 放在缓存前处理
        newCoupon.setTemplateSDK(acquireTemplateRequest.getCouponTemplateSdk());
        //加入到redis
        redisService.addCouponToCache(acquireTemplateRequest.getUserId(),
                Collections.singletonList(newCoupon),CouponStatus.USABLE.getCode());
        return newCoupon;
    }

    /**
     * 结算和核销优惠卷
     * @param settlementInfo {@link SettlementInfo}
     * @return
     * @throws BaseCloudException
     */
    @Override
    public SettlementInfo settlement(SettlementInfo settlementInfo) throws BaseCloudException {
        //当没有传递优惠卷是返回商品总价
        List<SettlementInfo.CouponAndTemplateInfo> couponAndTemplateInfos = settlementInfo.getCouponAndTemplateInfos();
        if(CollectionUtils.isEmpty(couponAndTemplateInfos)){
            log.info("This Time Not Make Coupons For settlement !");
            //单纯求和
            Double goodSums = settlementInfo.getGoodsInfos().stream().mapToDouble(g -> g.getPrice() * g.getCount())
                    .sum();
            settlementInfo.setCost(retain2Decimals(goodSums,2));
            return settlementInfo;
        }
        //效验优惠卷是否式自己的
        List<Coupon> coupons = findCouponsByStatus(settlementInfo.getUserId(), CouponStatus.USABLE.getCode());
        //转化容器类型
        Map<Integer, Coupon> id2Coupons = coupons.stream().collect(Collectors.toMap(Coupon::getId, Function.identity()));
        //判断该用户可使用优惠是否为空 或者 当前使用的优惠卷是否在可使用的优惠卷之间
        if(MapUtils.isEmpty(id2Coupons)||
        !CollectionUtils.isSubCollection(couponAndTemplateInfos
                .stream()
                .map(SettlementInfo.CouponAndTemplateInfo::getId)
                .collect(Collectors.toList()), id2Coupons.entrySet())){
            log.error("User Coupon Has Some Problem,It is not SubCollection of Coupons");
            throw new BaseCloudException(ExceptionResultData.USER_COUPON_HAS_SOME_PROBLEM);
        }
        log.debug("Current Settlement Coupons Is User : {}", couponAndTemplateInfos.size());
        //将即将处理的优惠卷记录下来
        List<Coupon> settleCoupons  = new ArrayList<Coupon>(couponAndTemplateInfos.size());
        couponAndTemplateInfos.forEach(coupon -> settleCoupons.add(id2Coupons.get(coupon.getId())));

        //通过结算服务获取结算信息 结算逻辑由结算服务处理
        SettlementInfo data = settlementClient.computeRule(settlementInfo).getData();

        //更新优惠卷状态
        if(data.getEmploy() && CollectionUtils.isNotEmpty(data.getCouponAndTemplateInfos())){
            log.info("Settle User Coupon : {} ,{}",data.getUserId(), JSON.toJSONString(data));
            //更新redis缓存 设置优惠卷为已使用
            redisService.addCouponToCache(data.getUserId(),settleCoupons,CouponStatus.USED.getCode());
            //更新db
            amqpTemplate.convertAndSend(Constant.RabbitMq.DISCOUNT_USER_COUPON_EXCHANGE,
                    Constant.RabbitMq.DISCOUNT_USER_COUPON_KEY,new CouponRabbitMessage(
                            CouponStatus.USED.getCode(),settleCoupons.stream().map(Coupon::getId).collect(Collectors.toList())
                    ));
        }


        return data;
    }

    /**
     * 保留位数
     * @param value 值
     * @param place 保留位数
     * @return
     */
    private Double retain2Decimals(Double value,Integer place){
        return new BigDecimal(value).setScale(place,BigDecimal.ROUND_HALF_UP).doubleValue();
    }
}
